package org.apache.ocean.server.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.ocean.Callback;

/**
 * Keeps all of a directory's files cached
 *
 * @author jasonr
 */
public class FileCache {
  public static Logger log = Logger.getLogger(FileCache.class.getName());
  private static final int DEFAULT_BUFFER_SIZE = 1024;
  public ConcurrentHashMap<String,FileData> map = new OceanConcurrentHashMap<String,FileData>(20);
  public File directory;
  Callback<FilesChangedEvent,Object> callback;
  long intervalMillis;
  ScheduledThreadPoolExecutor executor;
  
  public FileCache(File directory, Callback<FilesChangedEvent,Object> callback, long intervalMillis) {
    this.directory = directory;
    this.callback = callback;
    this.intervalMillis = intervalMillis;
    executor = (ScheduledThreadPoolExecutor)Executors.newSingleThreadScheduledExecutor();
    executor.scheduleWithFixedDelay(new HasChangedTask(), 0, intervalMillis, TimeUnit.MILLISECONDS);
  }
  
  public File toFile(String name) {
    return new File(directory, name);
  }
  
  public void close() {
    executor.shutdown();
  }
  
  public static class FilesChangedEvent {
    public Map<String,FileData> changed;
    public Map<String,FileData> deleted;
    public Map<String,FileData> news;
    
    public boolean hasChanges() {
      return (changed != null && changed.size() > 0
              && deleted != null && deleted.size() > 0
              && news != null && news.size() > 0);
    }
  }
  
  public String md5Hex(File file) throws Exception {
    FileInputStream fileInput = new FileInputStream(file);
    return md5Hex(fileInput);
  }
  
  public String md5Hex(InputStream input) throws Exception {
    MessageDigest messageDigest = getMd5Digest();
    DigestInputStream digestInput = new DigestInputStream(input, messageDigest);
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    long count = 0;
    int n = 0;
    while (-1 != (n = input.read(buffer))) {
      count += n;
    }
    byte[] encoded = messageDigest.digest();
    return new String(Hex.encodeHex(encoded));
  }
  
  static MessageDigest getDigest(String algorithm) {
    try {
      return MessageDigest.getInstance(algorithm);
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e.getMessage());
    }
  }
  
  private static MessageDigest getMd5Digest() {
    return getDigest("MD5");
  }
  
  public class HasChangedTask implements Runnable {
    public void run() {
      try {
        FilesChangedEvent filesChangedEvent = new FilesChangedEvent();
        Map<String,FileData> changed = new HashMap<String,FileData>();
        Map<String,FileData> deleted = new HashMap<String,FileData>();
        for (FileData fileData : map.values()) {
          File file = new File(directory, fileData.name);
          if (!file.exists()) {
            deleted.put(fileData.name, fileData);
          } else if (file.exists() && !StringUtils.equals(fileData.md5, md5Hex(file))) {
            changed.put(fileData.name, getFileData(fileData.name));
          }
        }
        File[] diskFiles = directory.listFiles();
        Map<String,File> diskFileMap = new HashMap<String,File>();
        for (int x=0; x < diskFiles.length; x++) {
          diskFileMap.put(diskFiles[x].getName(), diskFiles[x]);
        }
        Map<String,FileData> news = new HashMap<String,FileData>();
        for (File file : diskFileMap.values()) {
          String name = file.getName();
          if (!map.containsKey(name)) {
            FileData fileData = getFileData(name);
            news.put(file.getName(), fileData);
          }
        }
        filesChangedEvent.changed = changed;
        filesChangedEvent.deleted = deleted;
        filesChangedEvent.news = news;
        if (filesChangedEvent.hasChanges()) {
          callback.call(filesChangedEvent);
        }
      } catch (Throwable throwable) {
        log.log(Level.SEVERE, "", throwable);
      }
    }
  }
  
  public FileData getFileData(String name) throws Exception {
    File file = toFile(name);
    if (!file.exists()) return null;
    byte[] bytes = FileUtils.readFileToByteArray(file);
    String md5 = DigestUtils.md5Hex(bytes);
    FileData fileData = new FileData(name, md5, bytes);
    map.put(name, fileData);
    return fileData;
  }
  
  public byte[] getFileBytes(String name) throws Exception {
    FileData fileData = map.get(name);
    if (fileData == null) fileData = getFileData(name);
    if (fileData != null) return fileData.bytes;
    return null;
  }
  
  public static class FileData {
    public String name;
    public String md5;
    public byte[] bytes;
    
    public FileData(String name, String md5, byte[] bytes) {
      this.name = name;
      this.md5 = md5;
      this.bytes = bytes;
    }
  }
}
